﻿using System;
using System.IO;
using System.Text;
using System.Threading;
using DZTC.UselessAppEver.Core;

namespace DZTC.UselessAppEver.Base32
{
    internal class Encoder : Base32Converter
    {
        private static unsafe void EncodeChunk(byte* src, int length, byte* dst, byte* pEncodeTable)
        {
            // port of http://golang.org/src/pkg/encoding/base32/base32.go

            var srcLen = length;

            while (srcLen > 0)
            {
                // initializating output
                for (var i = 0; i < BASE32_QUANTUM_LENGTH; i++)
                    dst[i] = 0;

                switch (srcLen)
                {
                    default:
                        dst[7] |= (byte) (src[4] & 0x1F);
                        dst[6] |= (byte) (src[4] >> 5);
                        goto case 4; // fallthrough

                    case 4:
                        dst[6] |= (byte) ((src[3] << 3) & 0x1F);
                        dst[5] |= (byte) ((src[3] >> 2) & 0x1F);
                        dst[4] |= (byte) (src[3] >> 7);
                        goto case 3; // fallthrough

                    case 3:
                        dst[4] |= (byte) ((src[2] << 1) & 0x1F);
                        dst[3] |= (byte) ((src[2] >> 4) & 0x1F);
                        goto case 2; // fallthrough

                    case 2:
                        dst[3] |= (byte) ((src[1] << 4) & 0x1F);
                        dst[2] |= (byte) ((src[1] >> 1) & 0x1F);
                        dst[1] |= (byte) ((src[1] >> 6) & 0x1F);
                        goto case 1; // fallthrough

                    case 1:
                        dst[1] |= (byte) ((src[0] << 2) & 0x1F);
                        dst[0] |= (byte) (src[0] >> 3);
                        break; // fallthrough
                }


                // Encode 5-bit blocks using the base32 alphabet
                for (var i = 0; i < BASE32_QUANTUM_LENGTH; i++)
                {
                    dst[i] = pEncodeTable[dst[i]];
                }

                // Pad the final quantum
                if (srcLen < SOURCE_QUANTUM_LENGTH)
                {
                    dst[7] = PAD;

                    if (srcLen < 4)
                    {
                        dst[6] = PAD;
                        dst[5] = PAD;

                        if (srcLen < 3)
                        {
                            dst[4] = PAD;

                            if (srcLen < 2)
                            {
                                dst[3] = PAD;
                                dst[2] = PAD;
                            }
                        }
                    }

                    break;
                }

                src += SOURCE_QUANTUM_LENGTH;
                dst += BASE32_QUANTUM_LENGTH;
                srcLen -= SOURCE_QUANTUM_LENGTH;
            }
        }

        private static long GetChunkCount(long length)
        {
            var r = length%SOURCE_BUFFER_LENGTH;
            return length/SOURCE_BUFFER_LENGTH + (r != 0 || length == 0 ? 1 : 0); // at least one chunk, even if file is empty
        }


        internal static unsafe void EncodeInernal(Stream input, Stream output, CancellationToken ct, ProgressToken pt)
        {
            var src = new byte[SOURCE_BUFFER_LENGTH];
            var dst = new byte[BASE32_BUFFER_LENGTH];

            var chunkFork = 1d/GetChunkCount(input.Length);

            fixed (byte* pSrc = src)
            fixed (byte* pDst = dst)
            fixed (byte* pEncodeTable = ENCODE_TABLE)
            {
                while (true)
                {
                    using (pt.Fork(chunkFork))
                    {
                        ct.ThrowIfCancellationRequested();
                        var len = input.Read(src, 0, SOURCE_BUFFER_LENGTH);
                        if (len <= 0) break; // end

                        ct.ThrowIfCancellationRequested();
                        EncodeChunk(pSrc, len, pDst, pEncodeTable);

                        ct.ThrowIfCancellationRequested();
                        output.Write(dst, 0, (int)GetDestinationLength(len));

                        pt.Report(ProgressToken.MAX_VALUE);
                    }
                }
            }

            // 100%
            pt.Report(ProgressToken.MAX_VALUE);
        }



        internal static long GetDestinationLength(long len)
        {
            var r = len%SOURCE_QUANTUM_LENGTH;
            return (len/SOURCE_QUANTUM_LENGTH*BASE32_QUANTUM_LENGTH)
                   + (r != 0 ? BASE32_QUANTUM_LENGTH : 0);
        }
    }
}